今天大概會聊到的範圍
- Animation
 
上一次有聊到,我們可以透過 Gesture 和 State 來與 user 互動。例如下面這個例子:
@Composable
fun ExpandText() {
    var isExpanded by remember { mutableStateOf(false) }  // <--- 1. 
    
    Column(
        verticalArrangement = Arrangement.spacedBy(4.dp),
        modifier = Modifier.fillMaxHeight()
    ) {
    
        Row(modifier = Modifier.fillMaxWidth()) {
            Button(
                onClick = { isExpanded = !isExpanded },  // <--- 2.
            ) {
                Text("Expand")
            }
        }
        
        Box {
            Text(
                text = contentText,
                softWrap = true,
                maxLines = if (isExpanded) Int.MAX_VALUE else 1,   //  <--- 3.
                style = txtStyle,
                modifier = Modifier
                    .background(color = Color.White)
            )
        }        
    }
}
ExpandText是一個 Stateful 的 composable。在點擊2.的按鈕後,會修改1.的isExpandedState。3.這個Text的maxLine會依照isExpanded的狀態調整
在點擊按鈕後,會直接修改 Text 的 maxLine 屬性。Text 也會再一次的 recompisition 後就調整大小。

若我們希望可以做的動態一點,我們可以加上 Animation。以這個例子,我們可以在變動大小的 Text 身上加上 animateContentSize modifier。
@Composable
fun ExpandText() {
    var isExpanded by remember { mutableStateOf(false) }
    
    Column(
        verticalArrangement = Arrangement.spacedBy(4.dp),
        modifier = Modifier.fillMaxHeight()
    ) {
    
        Row(modifier = Modifier.fillMaxWidth()) {
            Button(
                onClick = { isExpanded = !isExpanded },
            ) {
                Text("Expand")
            }
        }
        
        Box {
            Text(
                text = contentText,
                softWrap = true,
                maxLines = if (isExpanded) Int.MAX_VALUE else 1,
                style = txtStyle,
                modifier = Modifier
                    .background(color = Color.White)
                    .animateContentSize()     // <--- 加上 animateContentSize 
            )
        }        
    }
}

在研究 Compose 的 Animation 時,發現 compose 提供了許多 high-level 的 animation 用法(也就是較多預先包裝好、常用的情境 )。增加這些 high-level animation 的方法並不太統一,但大致可以分成三大類:
Modifier.animateContentSize
用法大致可以分成上述三類,但卻因為使用情境不同,而用到不同的 animator。官方文件上有一個不錯的說明,我將它簡化列在這邊:
此動畫是否有需要改動到元件內容(元件大小、顏色、內容 ... )?
AnimationVisibility
Modifier.animateContentSize
AnimatedContent 或 Crossfade
此動畫會連動某個狀態(例如某個數值、顏色 ...)?
rememberInfiniteTransition 取代 StateupdateTransition 取代 Stateanimate*AsState 取代 State其他情境
Animatable  來執行動畫animateContentSize 稍早已經有介紹過了,當元件的大小會變動時可以增加這個 modifier,在大小變動時會自動以動畫的方式變動。
AnimatedVisibility, AnimatedContent, Crossfade 這三者需要以 Composable 的形式包裝在要變動的元件之外。以 AnimatedVisibility 為例:
AnimatedVisibility(
    visible = isExpanded
) {
    Text(
        text = contentText,
        softWrap = true,
        style = txtStyle,
        modifier = Modifier
            .background(color = Color.White)
    )
}
除了提供 visible 依據的 State 外,還可以提供 enter 與 exit 兩個參數來設定進場與出場的動畫。
AnimatedContent 和 AnimatedVisibility 的用法類似,要再 parameter 中提供一個 state。在 lambda 中的 composable 若也透過對應的 state 進行改變的話,在變動的過程就會套上動畫。也和 AnimatedVisibility 類似,可以調整 transitionSpec 來改變動畫。
如果在改變內容時,只是希望進行簡單的 fadeIn + fadeOut,則可以用 Crossfade 來取代AnimatedContent。
剛剛提到的,都是當 State A 改變到 State B 時,觸發一個動畫。動畫本身與 State A 和 B 的數值不相關。但有時候,我們需要以 State 作為動畫的數值,且需要將 State A ~ B 中間的間隔補齊。
舉個例,當我們需要改變一張卡片的顏色:
@Composable
fun AnimationScreen() {
    
    var color by remember { mutableStateOf(Color.White) }            // <--- 1. 
    val backgroundColor by animateColorAsState(targetValue = color)    // <--- 2.
    
    Column(
        verticalArrangement = Arrangement.spacedBy(4.dp),
        modifier = Modifier.fillMaxHeight()
    ) {
        
        Row(modifier = Modifier.fillMaxWidth()) {
            Button(onClick = { color = Color.Red }) { Text("Red") }
            Button(onClick = { color = Color.Green }) { Text("Green") }
            Button(onClick = { color = Color.Blue }) { Text("Blue") }
        }
        
        Text(
            text = contentText,
            softWrap = true,
            style = txtStyle,
            modifier = Modifier
                .background(color = backgroundColor)    // <--- 3.
        )
        
    }
}
animateXXXAsState 的 function 將這個 State 給包起來
Color, Dp, Size, 以及標準數值,例如 Int, Float 等
不過我們的確要修改 content 的內容啊?那能不能用 AnimatedContent 來達到效果呢? 答案其實是可以的,只是 default 的行為可能不盡理想。
因為 AnimatedContent 會變動整個元件,但是 animateColorAsState 只會將那一個值 ( color ) 做逐步的變動。
@Composable
fun AnimationScreen() {
    
    var color by remember { mutableStateOf(Color.White) }
    val backgroundColor by animateColorAsState(targetValue = color)
    
    Column(
        verticalArrangement = Arrangement.spacedBy(4.dp),
        modifier = Modifier.fillMaxHeight()
    ) {
        
        Row(modifier = Modifier.fillMaxWidth()) {
            Button(onClick = { color = Color.Red }) { Text("Red") }
            Button(onClick = { color = Color.Green }) { Text("Green") }
            Button(onClick = { color = Color.Blue }) { Text("Blue") }
        }
    
        Text("animate with animateColorAsState",
            style = titleStyle,
            modifier = Modifier.background(color = Color.White))
    
        Text(
            text = contentText,
            softWrap = true,
            style = txtStyle,
            modifier = Modifier
                .background(color = backgroundColor)
        )
        
        Spacer(modifier = Modifier.size(16.dp))
        
        Text("animate with AnimatedContent",
            style = titleStyle,
            modifier = Modifier.background(color = Color.White))
        
        AnimatedContent(targetState = color) {
            Text(
                text = contentText,
                softWrap = true,
                style = txtStyle,
                modifier = Modifier
                    .background(color = color)
            )
        }
        
    }
}

最後,如果當某個 State 要變動時,需要改變多個物件的屬性的話。可以使用 updateTransition 將 State 包起來,在透過 updateTransition.animateXXX 來異動不同的屬性
@Composable
fun AnimationScreen() {
        
    var isExpanded by remember { mutableStateOf(false) }    // <-- 1. State
    val transition = updateTransition(isExpanded)    // <-- 2. 包成 traistion
    
    // 3. 從 transition 中獨立出屬性
    val color by transition.animateColor {
        if(it) Color.Red else Color.Green
    }
    
    val maxLine by transition.animateInt {
        if (it) Int.MAX_VALUE else 2
    }
    
    
    
    Column(
        verticalArrangement = Arrangement.spacedBy(4.dp),
        modifier = Modifier.fillMaxHeight()
    ) {
        
        Row(modifier = Modifier.fillMaxWidth()) {
            Button(onClick = { isExpanded = !isExpanded }) { Text("expand") }
        }
        
        Text(
            text = contentText,
            softWrap = true,
            style = txtStyle,        
            maxLines = if (maxLine <= 0) 1 else maxLine,    // <--- 使用從 transition 中獨立出的屬性
            modifier = Modifier    
                .background(color = color)    // <--
        )
    }
}
今天沒有提到更詳細的 Animatable,但在 High-Level 的 animation function 支持下,基本的互動與動畫就已經可以執行。 今天最後的 maxLine 還有點瑕疵,需要特別判斷當 MAX_VALUE ~ 2 時,會經過 0 的情況(同時動畫也會因此 delay,也許用 MAX_VALUE 是很糟點子) 。Animation 這邊,應該還有很多的東西可以挖出來。
Reference: